<?php
/*********************************************
 *  CPG Dragonfly™ CMS
 *********************************************
	Copyright © since 2010 by CPG-Nuke Dev Team
	http://dragonflycms.org

	Dragonfly is released under the terms and conditions
	of the GNU GPL version 2 or any later version

	wiki.zope.org/ZPT/TALESSpecification13
*/

namespace Poodle\TPL;

abstract class Tales
{
	const
		DEFAULT_KEYWORD = '__DEFAULT__';

	# T_STRING
	private static
		$PHP_ALLOWED_FUNCTIONS = array(
			'count|empty|is_(array|bool|float|int|null|numeric|object|scalar|string)|isset|defined|extension_loaded',
			'Arrays' => 'array_sum|in_array|print_r',
			'Date/Time' => 'gmdate|date|time|date_default_timezone_get',
			'Filesystem' => 'basename|is_dir|is_file',
			'Math'   => 'ceil|floor|is_finite|is_infinite|is_nan|max|min|rand|round|srand',
			'PCRE'   => 'preg_(match|match_all|split)',
			'String' => 'bin2hex|explode|implode|json_encode|mb_(str|sub)[a-z_]+|nl2br|strtolower|strtoupper|substr|stri?pos|trim|ucfirst|ucwords|htmlspecialchars|strip_tags|strtotime|str_replace',
			'Dragonfly' => 'can_admin|sprintf|formatDateTime',
		);

	# php.net/tokens
	private static
		$PHP_ALLOWED_TOKENS = array(
//			T_ABSTRACT,
//			T_AND_EQUAL,
			T_ARRAY,
			T_ARRAY_CAST,
//			T_AS,
//			T_BAD_CHARACTER,
			T_BOOLEAN_AND,
			T_BOOLEAN_OR,
//			T_BOOL_CAST,
//			T_BREAK,
//			T_CALLABLE,
//			T_CASE,
			T_CATCH,
			T_CHARACTER,
//			T_CLASS,
//			T_CLASS_C,
//			T_CLONE,
//			T_CLOSE_TAG,
//			T_COMMENT,
//			T_CONCAT_EQUAL,
//			T_CONST,
			T_CONSTANT_ENCAPSED_STRING,
//			T_CONTINUE,
			T_CURLY_OPEN,
//			T_DEC,
//			T_DECLARE,
//			T_DEFAULT,
//			T_DIR,
//			T_DIV_EQUAL,
			T_DNUMBER,
//			T_DOC_COMMENT,
//			T_DO,
//			T_DOLLAR_OPEN_CURLY_BRACES,
//			T_DOUBLE_ARROW,
//			T_DOUBLE_CAST,
			T_DOUBLE_COLON,
//			T_ECHO,
//			T_ELSE,
//			T_ELSEIF,
			T_EMPTY,
			T_ENCAPSED_AND_WHITESPACE,
//			T_ENDDECLARE,
//			T_ENDFOR,
//			T_ENDFOREACH,
//			T_ENDIF,
//			T_ENDSWITCH,
//			T_ENDWHILE,
//			T_END_HEREDOC,
//			T_EVAL,
//			T_EXIT,
//			T_EXTENDS,
//			T_FILE,
//			T_FINAL,
//			T_FOR,
//			T_FOREACH,
//			T_FUNCTION,
//			T_FUNC_C,
//			T_GLOBAL,
//			T_GOTO,
//			T_HALT_COMPILER,
//			T_IF,
//			T_IMPLEMENTS,
//			T_INC,
//			T_INCLUDE,
//			T_INCLUDE_ONCE,
			T_INLINE_HTML,
			T_INSTANCEOF,
//			T_INSTEADOF,
//			T_INT_CAST,
//			T_INTERFACE,
			T_ISSET,
			T_IS_EQUAL,
			T_IS_GREATER_OR_EQUAL,
			T_IS_IDENTICAL,
			T_IS_NOT_EQUAL,
			T_IS_NOT_IDENTICAL,
			T_IS_SMALLER_OR_EQUAL,
//			T_LINE,
//			T_LIST,
			T_LNUMBER,
			T_LOGICAL_AND,
			T_LOGICAL_OR,
			T_LOGICAL_XOR,
//			T_METHOD_C,
//			T_MINUS_EQUAL,
//			T_MOD_EQUAL,
//			T_MUL_EQUAL,
//			T_NAMESPACE,
//			T_NS_C,
			T_NS_SEPARATOR,
//			T_NEW,
			T_NUM_STRING,
//			T_OBJECT_CAST,
			T_OBJECT_OPERATOR,
//			T_OPEN_TAG,
//			T_OPEN_TAG_WITH_ECHO,
//			T_OR_EQUAL,
			T_PAAMAYIM_NEKUDOTAYIM,
//			T_PLUS_EQUAL,
//			T_PRINT,
//			T_PRIVATE,
//			T_PUBLIC,
//			T_PROTECTED,
//			T_REQUIRE,
//			T_REQUIRE_ONCE,
//			T_RETURN,
//			T_SL,
//			T_SL_EQUAL,
//			T_SR,
//			T_SR_EQUAL,
//			T_START_HEREDOC,
//			T_STATIC,
			T_STRING,
//			T_STRING_CAST,
			T_STRING_VARNAME,
//			T_SWITCH,
//			T_THROW,
//			T_TRAIT,
//			T_TRAIT_C,
			T_TRY,
//			T_UNSET,
//			T_UNSET_CAST,
//			T_USE,
//			T_VAR,
			T_VARIABLE,
//			T_WHILE,
			T_WHITESPACE,
//			T_XOR_EQUAL,
		);

	/**
	 * translates TALES expression with alternatives into single PHP expression.
	 * e.g. "string:foo${bar}" may be transformed to "'foo'.tal_escape($ctx->bar)"
	 */
	public static function translate_expression($expression, $def=null)
	{
		$type = 'path';
		$expression = trim($expression);

		# Look for tales modifier (not:, path:, string:, php:, etc...)
		if (preg_match('#^([a-z][a-z0-9_]*[a-z0-9]):(.+)$#si', $expression, $m)) {
			list(, $type, $expression) = $m;
		}
		# may be a 'string'
		else if (preg_match('#^\'((?:[^\']|\\\\.)*)\'$#s', $expression, $m)) {
			$expression = stripslashes($m[1]);
			$type = 'string';
		}

		# is it a TALES expression modifier
		if (method_exists(__CLASS__, $type)) { return self::$type($expression, $def); }

		throw new \Exception("Unknown TALES modifier '$type'. Function '$func' does not exist");
	}

	protected static function not($expression, $def=null)
	{
		return '!(' . self::translate_expression($expression, $def) . ')';
	}

	/**
	 * path segments may contain: [a-zA-Z0-9\ _-.,~]
	 */
	protected static function path($expression, $def=null)
	{
		$expression = trim($expression);
		/**
		 * default  - special singleton value used by TAL to specify that existing text should not be replaced.
		 * nothing  - special singleton value used by TAL to represent a non-value (e.g. void, None, Nil, NULL).
		 * options  - the keyword arguments passed to the template.
		 * repeat   - the repeat variables (see RepeatVariable).
		 * attrs    - a dictionary containing the initial values of the attributes of the current statement tag.
		 * CONTEXTS - the list of standard names (this list). This can be used to access a builtin variable that
		 *            has been hidden by a local or global variable with the same name
		 */
		if ($expression === 'default') { return is_string($def) ? $def : self::DEFAULT_KEYWORD; }
		if ($expression === 'nothing') { return 'null'; }
		if ($expression == '')         { return 'null'; }

		$exps = explode('|', $expression);
		if (isset($exps[1])) {
			$result = array();
			foreach ($exps as $exp) { $result[] = self::translate_expression(trim($exp), $def); }
			return 'self::get_valid_option(array('.implode(',', $result).'))';
		}

		if ($p = strpos($expression, '/')) { return 'self::path($ctx->'.substr($expression,0,$p).','.self::string(substr($expression,$p+1)).')'; }
//		if (preg_match('#^([a-z_][a-z0-9_]*)/(.+)$#i', $expression, $m)) { return 'self::path($ctx->'.$m[1].','.self::string($m[2]).')'; }
		return '$ctx->'.$expression;
	}

	protected static $allowed_pre_tokens = array(T_WHITESPACE, T_INC);
	protected static function php($expression, $def=null)
	{
		if (is_array(self::$PHP_ALLOWED_FUNCTIONS)) { self::$PHP_ALLOWED_FUNCTIONS = '#^('.implode('|',self::$PHP_ALLOWED_FUNCTIONS).')$#i'; }

		$tokens = token_get_all('<?php '.$expression);
//		array_shift($tokens);
		// Fix custom ${context} format as it gets parsed by php
		$n_tokens = array();
		$c_tokens = count($tokens);
		for ($i=1; $i<$c_tokens; ++$i) {
			$t = $tokens[$i];
			if ('.' === $t) {
				$t = array(T_OBJECT_OPERATOR,'->');
			} else
			if ('$' === $t && '{' === $tokens[$i+1]) {
				while ('}' !== $tokens[++$i] && $i < $c_tokens) {
					$t .= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i];
				}
				$t .= $tokens[$i];
			}
			$n_tokens[] = $t;
		}
		$tokens = $n_tokens;

		$php = '';
		foreach ($tokens as $i => &$a) {
			if (is_array($a)) {
				$error = false;
				if (!in_array($a[0], self::$PHP_ALLOWED_TOKENS)) { $error = token_name($a[0]); }
				else if (T_STRING === $a[0] && isset($tokens[$i+1]) && '(' === $tokens[$i+1]
				 && (0==$i || !(is_array($tokens[$i-1]) && (T_DOUBLE_COLON === $tokens[$i-1][0] || T_OBJECT_OPERATOR === $tokens[$i-1][0])))
				 && !preg_match(self::$PHP_ALLOWED_FUNCTIONS, $a[1]))
				{
					$error = "function {$a[1]}()";
				}
				else if (T_VARIABLE === $a[0] && '$_GET'!==$a[1] && 0 === strpos($a[1], '$_') && !(is_array($tokens[$i+1]) && T_OBJECT_OPERATOR === $tokens[$i+1][0]))
				{
					$error = "T_VARIABLE {$a[1]}";
				}
				if ($error) {
//					trigger_error("TALES php: Disallowed {$error}", E_USER_WARNING);
//					return "'php:".str_replace('\'','\\\'',$php)."'";
					return "trigger_error('TALES php: Disallowed {$error} \\'{$a[1]}\\' in ".str_replace('\'','\\\'',$expression)."', E_USER_WARNING)";
				}
				if (T_STRING === $a[0] && isset($tokens[$i+1]) && T_OBJECT_OPERATOR === $tokens[$i+1][0])
				{
					if (0==$i || !is_array($tokens[$i-1]) || in_array($tokens[$i-1][0], self::$allowed_pre_tokens)) {
						if ('context'===$a[1]) $a[1] = '$ctx';
						else if ('here'===$a[1]) $a[1] = '$this';
						else if ('root'===$a[1]) $a[1] = '\\Poodle::getKernel()';
					}
				}
				# T_STRING                   "parent" 307
				# T_CONSTANT_ENCAPSED_STRING 'string' 315
				# T_STRING_VARNAME           "${a}"
				if (T_CONSTANT_ENCAPSED_STRING === $a[0])
				{
					$a = self::translate_expression($a[1]);
				}
			} else if ('`' === $a) { $a = '\''; }
			$php .= is_array($a) ? $a[1] : $a;
		}
		$tokens = null;

		if ($c = preg_match_all('#(\$+){([a-z0-9_/-]+)}#i', $php, $m, PREG_SET_ORDER))
		{
			foreach ($m as $match) {
				$s = strlen($match[1]);
				# push $$ as $
				$v = substr($match[1],0,floor($s/2));
				# process expression
//				if ($s % 2) { $v .= '\'.' . self::path($match[2]) . '.\''; }
				if ($s % 2) { $v .= self::path($match[2], $def); }
				$php = str_replace($match[0], $v, $php);
			}
		}

//		$php = preg_replace('#\.([a-z])#i', '->$1', $php);
//		$php = preg_replace('#(^|[^\pL\pN])context->#', '$ctx->', $php);
//		$php = preg_replace('#(^|[^\pL\pN])here->#', '$this->', $php);

		return $php;
		return str_replace(".''", '', "'{$php}'");
	}

	/**
	 * string:
	 *
	 *      string_expression ::= ( plain_string | [ varsub ] )*
	 *      varsub            ::= ( '$' Path ) | ( '${' Path '}' )
	 *      plain_string      ::= ( '$$' | non_dollar )*
	 *      non_dollar        ::= any character except '$'
	 *
	 * Examples:
	 *
	 *      string:my string
	 *      string:hello, $username how are you
	 *      string:hello, ${user/name}
	 *      string:you have $$130 in your bank account
	 */
	public static function string($expression, $def=null)
	{
		$expression = addcslashes($expression, "'\\");
		if ($c = preg_match_all('#(\$+)([a-z0-9_/]+|{([a-z0-9_/]+)})#i', $expression, $m, PREG_SET_ORDER))
		{
			foreach ($m as $match) {
				$s = strlen($match[1]);
				# push $$ as $
				$v = substr($match[1],0,floor($s/2));
				# process expression
				if ($s % 2) { $v .= '\'.' . self::path(empty($match[3])?$match[2]:$match[3], $def) . '.\''; }
				$expression = str_replace($match[0], $v, $expression);
			}
		}
		return str_replace(".''",'', "'{$expression}'");
	}

}
